Concevoir un premier modèle de segmentation d’images pour Future Vision Transport - une entreprise qui conçoit des systèmes embarqués de vision par ordinateur pour les véhicules autonomes. Le modèle de segmentation devra s’intégrer facilement dans la chaîne complète du système embarqué.
Techniquement parlant, notre responsabilité est de se charger de la segmentation des images. Plus précisément :
L'objectif final est donc de créer une interface qui affichera les images et les masks.
Ce notebook a pour but un prétraitement de données.
import glob
from distutils.dir_util import copy_tree
import os
import shutil
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2
#from skimage import io, color
#from skimage.color import rgba2rgb, rgb2gray
import tqdm
import random
from random import shuffle
import time
import imgaug as ia
import imgaug.augmenters as iaa
import tensorflow as tf
from tensorflow.keras.preprocessing import image
from tensorflow.keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
from tensorflow.keras import backend as K
from tensorflow.keras.optimizers import Adadelta, Nadam, Adam
from tensorflow.keras.models import Model, load_model, Sequential
from tensorflow.keras.utils import plot_model, Sequence, to_categorical
from tensorflow.keras.callbacks import TensorBoard, ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, Callback
from tensorflow.keras.losses import binary_crossentropy
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, concatenate, Conv2DTranspose, UpSampling2D, BatchNormalization, Activation, Dropout, Reshape, Permute, Dense, GlobalAveragePooling2D
import segmentation_models as sm
from segmentation_models import Unet
from segmentation_models import get_preprocessing
from segmentation_models.losses import bce_jaccard_loss, DiceLoss, JaccardLoss, CategoricalCELoss
from segmentation_models.metrics import iou_score, FScore
2022-11-10 11:29:04.988003: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags. 2022-11-10 11:29:05.702888: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64: 2022-11-10 11:29:05.702954: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine. 2022-11-10 11:29:05.794243: E tensorflow/stream_executor/cuda/cuda_blas.cc:2981] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered 2022-11-10 11:29:07.229478: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64: 2022-11-10 11:29:07.229739: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64: 2022-11-10 11:29:07.229753: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
Segmentation Models: using `keras` framework.
# Chemin vers les données
PATH = '/home/sylwia/Jupyter/P8_Segm_sem'
#pip show tensorflow
Name: tensorflow Version: 2.10.0 Summary: TensorFlow is an open source machine learning framework for everyone. Home-page: https://www.tensorflow.org/ Author: Google Inc. Author-email: packages@tensorflow.org License: Apache 2.0 Location: /home/sylwia/anaconda3/lib/python3.9/site-packages Requires: absl-py, astunparse, flatbuffers, gast, google-pasta, grpcio, h5py, keras, keras-preprocessing, libclang, numpy, opt-einsum, packaging, protobuf, setuptools, six, tensorboard, tensorflow-estimator, tensorflow-io-gcs-filesystem, termcolor, typing-extensions, wrapt Required-by: Note: you may need to restart the kernel to use updated packages.
pip list | grep Keras
Keras-Applications 1.0.8 Keras-Preprocessing 1.1.2 Note: you may need to restart the kernel to use updated packages.
Le jeu de données à notre dispostion - Cityscapes - contient de diverses scènes de paysages urbains enregistrées à différents moments de l'année et venant de 50 villes différentes.
Le jeu de données contient 25 000 photos, pourvues de masks de segmentations (autrement dit annotations). 5000 de ces masks sont finement annotés, tandis que les 20 000 restantes sont beaucoup plus approximatifs. Pour notre projet, nous allons travailler avec les photographies disposant d'un mask de catégories finement annoté.
Les paires images - mask sont pré-divisées en train (2975 images), validation (500 images) et test (1525 images) sets.
Enfin, les photographies et les masks qui leur sont associés sont tous proposés en résolution 2048 x 1024.
Affichons les sets regroupant les villes respectives :
# Datasets
!ls ../P8_Segm_sem/data/
dataset gtFine leftImg8bit models
# Images
print("Images pour l'entraînement :")
!ls ../P8_Segm_sem/data/leftImg8bit/train
print("\nImages pour validation :")
!ls ../P8_Segm_sem/data/leftImg8bit/val
print("\nImages pour tests :")
!ls ../P8_Segm_sem/data/leftImg8bit/test
Images pour l'entraînement : aachen cologne erfurt jena strasbourg ulm bochum darmstadt hamburg krefeld stuttgart weimar bremen dusseldorf hanover monchengladbach tubingen zurich Images pour validation : frankfurt lindau munster Images pour tests : berlin bielefeld bonn leverkusen mainz munich
# Masques
print("Masques pour l'entraînement :")
!ls ../P8_Segm_sem/data/gtFine/train
print("\nMasques pour validation :")
!ls ../P8_Segm_sem/data/gtFine/val
print("\nMasques pour tests :")
!ls ../P8_Segm_sem/data/gtFine/test
Masques pour l'entraînement : aachen cologne erfurt jena strasbourg ulm bochum darmstadt hamburg krefeld stuttgart weimar bremen dusseldorf hanover monchengladbach tubingen zurich Masques pour validation : frankfurt lindau munster Masques pour tests : berlin bielefeld bonn leverkusen mainz munich
La segmentation d'image sémantique est issue du domaine scientifique Computer vision. Il s'agit d'étiqueter des endroits spécifiques d'une image en fonction de ce qu'elle affiche : le modèle partitionne l'image en segments, chacun d'eux représentant une entité différente. Le résultat de la segmentation est donc une partition sémantique de l'image.
En effet, la segmentation sémantique consiste à étiqueter chaque pixel d’une image avec une classe correspondante à ce qui est représenté. On parle aussi de prédiction dense, car chaque pixel doit être prédit.
Le jeu de données fournit les images d'origine (ground truth) et leurs masks (images labellisées). Notre tâche consistera à en constituer un dataset que l'on pourra soumettre au modèle de machine learning. Pour cela, il faudra extraire les masks - (les images avec le suffixe labelIds) et les "coupler" les masks avec les images ground truth.
Deux problèmes à affronter lors de l'extraction de masques :
_gtFine_labelIds. L'extraction se fera avec le module shutil, qui aide à automatiser les processus de manipulation de fichiers et de répertoires. La méthode shutil.copy2() sera utilisée pour copier le contenu du fichier source dans le fichier ou le répertoire de destination.
# Fonction pour identifier et extraire puis renommer les images-cibles
def extract_rename_file(directory, destination, sent_id) :
for file in glob.iglob(f'{directory}/**/*'):
if file.endswith(sent_id):
filepath = os.path.join(directory, file)
newfilepath = os.path.join(directory, file.replace(sent_id, ".png"))
os.rename(filepath, newfilepath)
shutil.copy2(newfilepath, destination)
# Path pour identifier des images-cibles
sent_id = "_gtFine_labelIds.png"
# Fichier source
directory = PATH + '/data/gtFine/train/'
destination = PATH + r'/data/dataset/train_masks'
#extract_rename_file(directory, destination, sent_id)
# Fichier source
directory = PATH + '/data/gtFine/val/'
destination = PATH + r'/data/dataset/val_masks'
#extract_rename_file(directory, destination, sent_id)
# Fichier source
directory = PATH + '/data/gtFine/test/'
destination = PATH + r'/data/dataset/test_masks'
#extract_rename_file(directory, destination, sent_id)
A présent, il est nécessaire de renommer les fichiers pour qu'ils soient identiques pour les images et les masques.
# Images
!ls ../P8_Segm_sem/data/leftImg8bit
test train val
directory = PATH + r'/data/leftImg8bit/train/'
destination = PATH + r'/data/dataset/train_img'
sent_id = "_leftImg8bit.png"
#extract_rename_file(directory, destination, sent_id)
directory = PATH + r'/data/leftImg8bit/val/'
destination = PATH + r'/data/dataset/val_img'
sent_id = "_leftImg8bit.png"
#extract_rename_file(directory, destination, sent_id)
directory = PATH + r'/data/leftImg8bit/test/'
destination = PATH + r'/data/dataset/test_img'
sent_id = "_leftImg8bit.png"
#extract_rename_file(directory, destination, sent_id)
# C'EST PAR ICI
Vérifions si nous avons bien le même nombre de masques et d'images dans les dossiers train, validation et test. Tout d'abord, créons les variables contenant les chemins d'accès :
!ls ../P8_Segm_sem/data/dataset/
dataset_compr.zip test_masks train_masks val_masks test_img train_img val_img
Création de variables contenant le chemin d'accès vers les datasets respectifs :
train_images_dir = PATH +'/data/dataset/train_img'
train_masks_dir = PATH +'/data/dataset/train_masks'
val_images_dir = PATH +'/data/dataset/val_img'
val_masks_dir = PATH +'/data/dataset/val_masks'
test_images_dir = PATH +'/data/dataset/test_img'
test_masks_dir = PATH +'/data/dataset/test_masks'
Création de variables contenant l'ensemble d'images des datasets respectifs :
# Train set : images + masques
train_image_list = os.listdir(train_images_dir)
train_mask_list = os.listdir(train_masks_dir)
train_image_list.sort()
train_mask_list.sort()
train_mask_list[:3]
['aachen_000000_000019.png', 'aachen_000001_000019.png', 'aachen_000002_000019.png']
# Validation set : images + masques
val_image_list = os.listdir(val_images_dir)
val_mask_list = os.listdir(val_masks_dir)
val_image_list.sort()
val_mask_list.sort()
# Trest set : images + masques
test_image_list = os.listdir(test_images_dir)
test_mask_list = os.listdir(test_masks_dir)
test_image_list.sort()
test_mask_list.sort()
Affichons à présent le contenu des datasets - combien d'images et masques ? Sont-ils alignés ?
# Fonction qui permet de trier et afficher le volume + 10 premiers exemples
def check_volum(img_list, mask_list) :
# Affichage longueur
print(f'Number of images: {len(img_list)}\nNumber of masks: {len(mask_list)}\n')
# Affichage 10 premiers exemples
for input_path, target_path in zip(img_list[:10], mask_list[:10]) :
print(input_path, " | ", target_path)
Train
check_volum(train_image_list, train_mask_list)
Number of images: 2975 Number of masks: 2975 aachen_000000_000019.png | aachen_000000_000019.png aachen_000001_000019.png | aachen_000001_000019.png aachen_000002_000019.png | aachen_000002_000019.png aachen_000003_000019.png | aachen_000003_000019.png aachen_000004_000019.png | aachen_000004_000019.png aachen_000005_000019.png | aachen_000005_000019.png aachen_000006_000019.png | aachen_000006_000019.png aachen_000007_000019.png | aachen_000007_000019.png aachen_000008_000019.png | aachen_000008_000019.png aachen_000009_000019.png | aachen_000009_000019.png
Validation
check_volum(val_image_list, val_mask_list)
Number of images: 500 Number of masks: 500 frankfurt_000000_000294.png | frankfurt_000000_000294.png frankfurt_000000_000576.png | frankfurt_000000_000576.png frankfurt_000000_001016.png | frankfurt_000000_001016.png frankfurt_000000_001236.png | frankfurt_000000_001236.png frankfurt_000000_001751.png | frankfurt_000000_001751.png frankfurt_000000_002196.png | frankfurt_000000_002196.png frankfurt_000000_002963.png | frankfurt_000000_002963.png frankfurt_000000_003025.png | frankfurt_000000_003025.png frankfurt_000000_003357.png | frankfurt_000000_003357.png frankfurt_000000_003920.png | frankfurt_000000_003920.png
Test
check_volum(test_image_list, test_mask_list)
Number of images: 1525 Number of masks: 1525 berlin_000000_000019.png | berlin_000000_000019.png berlin_000001_000019.png | berlin_000001_000019.png berlin_000002_000019.png | berlin_000002_000019.png berlin_000003_000019.png | berlin_000003_000019.png berlin_000004_000019.png | berlin_000004_000019.png berlin_000005_000019.png | berlin_000005_000019.png berlin_000006_000019.png | berlin_000006_000019.png berlin_000007_000019.png | berlin_000007_000019.png berlin_000008_000019.png | berlin_000008_000019.png berlin_000009_000019.png | berlin_000009_000019.png
Les noms d'images et masques correspondent comme prévu. Affichons quelques photos et leurs masques :
def show_img_mask_idx(i, train_images_dir, train_masks_dir) :
my_image = img_to_array(load_img(
f'{train_images_dir}/{train_image_list[i]}'))/255.
my_mask = img_to_array(load_img(
f'{train_masks_dir}/{train_mask_list[i]}', color_mode="grayscale"))
# Définition du nb de labels
labels = np.unique(my_mask)
print('\nCombien de labels sur cette image :', len(labels))
my_mask = np.squeeze(my_mask)
fig = plt.figure(figsize=(15, 15))
ax = fig.add_subplot(1, 2, 1)
ax.set_title('Image')
ax.imshow(my_image)
plt.axis("off")
ax1 = fig.add_subplot(1, 2, 2)
ax1.set_title('Mask')
ax1.imshow(my_mask)
plt.axis("off")
plt.show()
i = 100
show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 11
i = 101
show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 16
i = 110
show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 18
Accuracy n'est pas une bonne métrique à utiliser dans une segmentation sémantique, car elle ne reflète pas le partitionnement des endroits d'une image. Il existe deux mesures permettant de mesurer cela :
Pour les deux mesures, on obtient un score entre 0 (pas d’intersection du tout) et 1 (masques identiques).
La métrique que nous avons surveillée de près est l’IoU. C'est la métrique de référence utilisée pour comparer les modèles de l'état de l'art. Nous avons noté également les scores F1 (Dice) tout au long de nos tests.
Les données nécessitent un prétraitement. Plusieurs points importants sont à soulever :
Le jeu de données propose des annotations qui couvrent 8 catégories et 32 sous-catégories. Il s'agit d'objets couramment rencontrées lors d'une conduite.
Notre objectif est de se focaliser sur les 8 catégories suivantes :
Les « masks » de catégories étant prévus pour 30 catégories, il va falloir les convertir pour qu'ils représentent les 8 catégories principales. Chacune des 30 catégories sera donc affectée à l'une des 8 nouvelles catégories.
Essayons de les afficher :
mask_train_path = PATH + "/data/dataset/train_masks"
cats = {'void': [0, 1, 2, 3, 4, 5, 6],
'flat': [7, 8, 9, 10],
'construction': [11, 12, 13, 14, 15, 16],
'object': [17, 18, 19, 20],
'nature': [21, 22],
'sky': [23],
'human': [24, 25],
'vehicle': [26, 27, 28, 29, 30, 31, 32, 33, -1]}
# Creation of mask from grey scale image
def create_mask(img, cats):
'''creates an mask from image and segmentation categories
Args:
img - PIL image
cats - dict {'cat1':[value1,value2,value3,etc...],'cat2':[value1,value2,value3,etc...]}
Returns:
A mask of type np.array of dimension (shape(img),len(cats)) '''
img = tf.keras.preprocessing.image.img_to_array(img,dtype=np.int32) # convert img to np.array
img = np.squeeze(img) #remove 1 dimension
mask = np.zeros((img.shape[0], img.shape[1], len(cats)),dtype=int) # create a mask with zeros
flat_cat = [val for cat in list(cats.values()) for val in cat] # create a list of all values associated with categories
ca_min = min(flat_cat)
ca_max = max(flat_cat)
cats_names = list(cats.keys())
#for each values associated with a category, fill in the mask with the corresponding category number
for i in range(ca_min,ca_max):
for idx,name in enumerate(cats_names):
if i in cats[name]:
mask[:,:,idx] = np.logical_or(mask[:,:,idx],(img==i))
#print('mask',mask)
return mask
# Extraction de catégories via un tensor [:,:,i]
categories=[]
for file in train_mask_list :
mask_file = mask_train_path +'/'+ file
mask = tf.keras.preprocessing.image.load_img(mask_file,target_size=(128,256),color_mode="grayscale")
mask_tensor = create_mask(mask,cats)
cat=[]
for i in range(8):
cat.append(mask_tensor[:,:,i].sum())
categories.append(cat)
# Conversion en array
categories=np.array(categories)
# Comptage sur l'axe 0
print(categories.sum(axis=0))
[10476029 37744963 21198078 1714486 14632973 3406053 1166331 6789915]
Affichons la répartition des 8 catégories principales :
# Affichage des catégories à l'aide d'un barplot
plt.figure(figsize=(10,6))
plt.bar(x=cats.keys(),
height=categories.sum(axis=0),
tick_label=list(cats.keys())
)
plt.xticks(rotation=45, fontsize=12)
plt.title('Répartition des classes\n', fontsize=14)
plt.tight_layout()
Les 8 catégories sont donc réparties de façon assez inégale. De quels types d'objets s'agit-il concrètement ? :
Etant donné la nature de l’input, il faut trouver un moyen pour ne pas charger toutes les images pour l'entraînement. La solution est la classe Data Generator.
La classe Data Generator a permis de distribuer les images par batches (de taille paramétrable) et en parallèle appliquer les éventuels pré-processings (attribution des 8 classes au lieu des 32 sous-catégories) ou augmentation de données.
Une classe Dataset doit implémenter trois fonctions : init, len et getitem :
# The numpy.logical_or() method is used to c alculate truth values between x1 and x2 element-wise
# Classe for data loading and preprocessing
class CustomDataset:
"""Read images & preprocess images.
Args:
images_dir (str): path to images folder
masks_dir (str): path to segmentation masks folder
"""
def __init__(
self,
images_dir,
masks_dir
):
self.ids = os.listdir(images_dir)
self.images_fps = [os.path.join(images_dir, image_id) for image_id in self.ids]
self.masks_fps = [os.path.join(masks_dir, image_id) for image_id in self.ids]
def __len__(self):
return len(self.ids)
def __getitem__(self, i):
# read & preprocess data
image = cv2.imread(self.images_fps[i])
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = image/255.0 # normalisation ! RGB images go from 0 to 255
image = np.asarray(image).astype(np.float32)
mask_ = cv2.imread(self.masks_fps[i], 0)
mask_ = np.squeeze(mask_)
mask = np.zeros((mask_.shape[0], mask_.shape[1], 8))
for i in range(0, 34):
if i in cats['void']:
mask[:,:,0] = np.logical_or(mask[:,:,0],(mask_==i))
elif i in cats['flat']:
mask[:,:,1] = np.logical_or(mask[:,:,1],(mask_==i))
elif i in cats['construction']:
mask[:,:,2] = np.logical_or(mask[:,:,2],(mask_==i))
elif i in cats['object']:
mask[:,:,3] = np.logical_or(mask[:,:,3],(mask_==i))
elif i in cats['nature']:
mask[:,:,4] = np.logical_or(mask[:,:,4],(mask_==i))
elif i in cats['sky']:
mask[:,:,5] = np.logical_or(mask[:,:,5],(mask_==i))
elif i in cats['human']:
mask[:,:,6] = np.logical_or(mask[:,:,6],(mask_==i))
elif i in cats['vehicle']:
mask[:,:,7] = np.logical_or(mask[:,:,7],(mask_==i))
return image, mask
# Function pour visualiser les images
def visualize(**images):
"""PLot images in one row."""
n = len(images)
plt.figure(figsize=(20, 10))
for i, (name, image) in enumerate(images.items()):
plt.subplot(1, n, i + 1)
plt.xticks([])
plt.yticks([])
plt.title(' '.join(name.split('_')).title())
plt.imshow(image, cmap=plt.cm.bone)
plt.show()
Regardons si cela fonctionne :
# Génération du dataset
tf_dataset = CustomDataset(train_images_dir, train_masks_dir)
type(tf_dataset)
__main__.CustomDataset
# for image, mask in tf_dataset :
# print(image[:1][0][0], "\n", mask[:1][0][0])
image, mask = tf_dataset[3]
print('Image shape:', image.shape)
print('Mask shape:', mask.shape)
Image shape: (1024, 2048, 3) Mask shape: (1024, 2048, 8)
def visualize_img_mask(img, mask) :
plt.figure(figsize=(8, 6))
plt.subplot(1, 1, 1)
plt.title("Image")
print(img.shape)
plt.imshow(img)
plt.axis('off')
plt.show()
plt.figure(figsize=(18, 12))
plt.subplot(1, 2, 2)
plt.title("Mask")
mask_max = np.argmax(mask, axis=2)
print(mask_max.shape)
plt.imshow(mask_max)
plt.axis('off')
plt.show()
visualize_img_mask(image, mask)
(1024, 2048, 3)
(1024, 2048)
visualize(
image=image,
# Img_R=image[..., 0].squeeze(),
# Img_G=image[..., 1].squeeze(),
# Img_B=image[..., 2].squeeze(),
void=mask[..., 0].squeeze(),
flat=mask[..., 1].squeeze(),
construction=mask[..., 2].squeeze(),
obiect=mask[..., 3].squeeze(),
nature=mask[..., 4].squeeze(),
sky=mask[..., 5].squeeze(),
human=mask[..., 6].squeeze(),
vehicle=mask[..., 7].squeeze()
)
print('image_shape :',image.shape)
print('mask_shape :',mask.shape)
image_shape : (1024, 2048, 3) mask_shape : (1024, 2048, 8)
Il est important de tenir compte de l'instabilité du système d'acquisition d'images. Il s'agit d'images enregistrées à différents moments de l'année dans un paysage urbain, nous avons donc affaire à un grand flux de données entrant qui, de plus, est instable.
Pour remédier à ce problème, nous allons essayer de reproduire ces conditions en ayant recours d'une part à des techniques d'augmentation des données, et d'autre part à un générateur de données.
Comment reproduire le comportement d'instabilité d'acquisition d'images ? Il faut pouvoir réaliser des transformations sur les images (ainsi que sur les masques de segmentation). L'objectif est de changer l'emplacement des pixels, c'est-à-dire créer des images artificielles en ayant recours à la Data Augmentation.
L’idée derrière la Data Augmentation est de reproduire les données préexistantes en leur appliquant une transformation aléatoire. Plusieurs avantages suite à cela :
Lors de l’entraînement, notre modèle apprendra sur beaucoup plus de données tout en ne rencontrant jamais deux fois la même image.
Nous allons utiliser pour cela la librairie IMGAUG qui permet de nombreuses transformations.
Il existe plusieurs types de transformations :
Gaussian blur
Un flou gaussien est le résultat du floutage d'une image par une fonction gaussienne.
def blur_img(img):
n = random.uniform(0,2.0) # Generating random value for sigma
blur_transform = iaa.Sequential([iaa.GaussianBlur(sigma=n)])
img_blur = blur_transform(image=tf.keras.preprocessing.image.img_to_array(img))
return tf.keras.preprocessing.image.array_to_img(img_blur)
Random zoom
Permet de zoomer de manière aléatoire à l’intérieur des images.
def zoom_img(img, mask):
n = random.uniform(1, 5) # Generating random value for sigma
# uses order=0 to avoid artifacts in mask
zoom_transform = iaa.Sequential([iaa.Affine(scale=n, order=0)])
img_zoom = zoom_transform(
image=tf.keras.preprocessing.image.img_to_array(img))
mask_zoom = zoom_transform(
image=tf.keras.preprocessing.image.img_to_array(mask))
return tf.keras.preprocessing.image.array_to_img(img_zoom), tf.keras.preprocessing.image.array_to_img(mask_zoom, scale=False)
Random brightness
Permet de modifier la luminosité d'une image de manière aléatoire.
def brightness_img(img):
br_transform = iaa.Sequential([iaa.MultiplyBrightness((4,2))])
img_br = br_transform(image=tf.keras.preprocessing.image.img_to_array(img).astype(np.uint8))
return tf.keras.preprocessing.image.array_to_img(img_br,scale=False)
Horizontal flip
Retourne horizontalement des images de manière aléatoire (autrement dit certaines seront retournées et d’autres non).
def flip_img(img,mask):
flip_transform = iaa.Sequential([iaa.Fliplr()])
img_flip = flip_transform(image=tf.keras.preprocessing.image.img_to_array(img))
mask_flip = flip_transform(image=tf.keras.preprocessing.image.img_to_array(mask))
return tf.keras.preprocessing.image.array_to_img(img_flip), tf.keras.preprocessing.image.array_to_img(mask_flip,scale=False)
Résultats
Affichons les images suite à la transformation aléatoire :
def show_image(item) :
fig = plt.figure(figsize=(8, 12))
plt.imshow(item)
plt.axis("off")
# Annotations
!ls ../P8_Segm_sem/data/dataset/
dataset_compr.zip test_masks train_masks val_masks test_img train_img val_img
# Images pour tests
test_img_file="data/dataset/val_img/munster_000116_000019.png"
test_mask_file="data/dataset/val_masks/munster_000116_000019.png"
# Loading images
img = tf.keras.preprocessing.image.load_img(test_img_file,target_size=(128,256))
mask = tf.keras.preprocessing.image.load_img(test_mask_file,target_size=(128,256),color_mode="grayscale")
Image de départ :
show_image(item=img)
Blur
show_image(item=blur_img(img))
Zoom
zooms = zoom_img(img,mask)
show_image(item=zooms[0])
Brightness
show_image(brightness_img(img))
Flip
flips = flip_img(img,mask)
show_image(flips[0])
Les générateurs sont des fonctions dans lequelles on stocke des variables, des éléments ou encore des images.
Les images étant volumineuses par essence, il est difficile de toutes les charger en mémoire pour entraîner nos modèles. J'ai donc mis en place un générateur de données adapté au jeu de données pour éviter ce problème.
Cette méthode est particulièrement pratique pour l’entraînement d’un modèle de Deep Learning qui fonctionne sur des lots de donnée(batch). Les lots sont chargés seulement lorsque le modèle en a besoin (par itération).
La classe CustomDataGenerator distribue donc les images par batch de taille paramétrable et en profite pour appliquer les éventuels pré-processing ou augmentations de données qui lui ont été transmis.
L’avantage des générateurs c’est qu’ils ne calculent pas la valeur de chaque élément. En effet, ils calculent les éléments uniquement lorsqu’on leur demande de le faire. C’est ce qu’on appelle une évaluation paresseuse (lazy evaluation). Elle permet d’utiliser immédiatement les données déjà calculées, pendant que le reste des données est en cours de calcul.
Les générateurs permettent donc un gain de rapidité et d’espace mémoire !
Les paramètres que l’on utilise :
L'objectif est donc de constituer une liste composée du batch de données et de l'image. Nous obtenons en sortie un lot d'images de taille self.batch et et un tableau sous forme [image_batch, GT].
def normalize_input_img(img):
'''Normalize PIL image to fall in [-1,1] range, returns 3D numpy array'''
img =tf.keras.preprocessing.image.img_to_array(img,dtype=np.int32)
img = img/255.
img -= 1
return img
class CustomDataGenerator(Sequence):
def __init__(self, image_dir,
mask_dir,
batch_size,
img_height,
img_width,
cats,
sample_perc=100,
aug_blur=False,
aug_zoom=False,
aug_brightness=False,
aug_flip=False,
#augmentation=None,
preprocessing=None
):
self.image_dir = image_dir
self.mask_dir = mask_dir
self.image_filename = os.listdir(image_dir)
self.image_filename.sort()
self.mask_filename = os.listdir(mask_dir)
self.mask_filename.sort()
self.sample_perc = sample_perc
# Generate a sample
rdm_index = random.sample(range(0, len(self.image_filename)), int(
len(self.image_filename)*self.sample_perc/100))
image_filename_sample = []
for i in rdm_index:
image_filename_sample.append(self.image_filename[i])
mask_filename_sample = []
for i in rdm_index:
mask_filename_sample.append(self.mask_filename[i])
self.image_filename = image_filename_sample
self.mask_filename = mask_filename_sample
self.batch_size = batch_size
self.img_height = img_height
self.img_width = img_width
self.cats = cats
self.sample_perc = sample_perc
self.aug_blur = aug_blur
self.aug_zoom = aug_zoom
self.aug_brightness = aug_brightness
self.aug_flip = aug_flip
#self.augmentation = augmentation
self.preprocessing = preprocessing
def __len__(self):
return int(np.ceil(len(self.image_filename) / float(self.batch_size)))
def __getitem__(self, idx):
# generate random index for the batch
idx = np.random.randint(0, len(self.image_filename)-1, self.batch_size)
batch_img, batch_mask = [], []
for i in idx:
# filename
img_file = self.image_dir+'/'+self.image_filename[i]
mask_file = self.mask_dir+'/'+self.mask_filename[i]
# Load as PIL and resize
img = tf.keras.preprocessing.image.load_img(
img_file, target_size=(self.img_height, self.img_width))
mask = tf.keras.preprocessing.image.load_img(
mask_file, target_size=(128, 256), color_mode="grayscale")
# Normalize image and create mask from greyscale image
img_norm = normalize_input_img(img)
mask_tensor = create_mask(mask, self.cats)
# Add to the batch
batch_img.append(img_norm)
batch_mask.append(mask_tensor)
if self.aug_blur:
batch_img.append(normalize_input_img(blur_img(img)))
# When using blur augmentation,the mask is not changed
batch_mask.append(mask_tensor)
if self.aug_zoom:
zooms = zoom_img(img, mask)
batch_img.append(normalize_input_img(zooms[0]))
batch_mask.append(create_mask(zooms[1], self.cats))
if self.aug_brightness:
batch_img.append(normalize_input_img(brightness_img(img)))
# When using brightness augmentation,the mask is not changed
batch_mask.append(mask_tensor)
if self.aug_flip:
flips = flip_img(img, mask)
batch_img.append(normalize_input_img(flips[0]))
batch_mask.append(create_mask(flips[1], self.cats))
if self.preprocessing:
batch_img = self.preprocessing(batch_img)
return np.array(batch_img, dtype=float), np.array(batch_mask, dtype=float)
BATCH_SIZE=4
train_custom = CustomDataGenerator(train_images_dir, train_masks_dir,
BATCH_SIZE, 128, 256, cats,
aug_blur=False, aug_zoom=False, aug_brightness=False, aug_flip=False)
Combien de batches ?
C'est la méthode __len__ qui calcule le nombre de lots que notre générateur est censé produire. Pour cela, nous divisons le nombre total d'échantillons par batch_size et ainsi nous obtenons la valeur finale de batches.
len(train_custom)
744
batch_img, batch_mask = train_custom.__getitem__(100)
print(batch_img.shape, batch_mask.shape)
(4, 128, 256, 3) (4, 128, 256, 8)
On a bien des lots de 4 images de dimensions 128x256x3 et des lots de 4 masks à 8 couleurs (correspondant à nos catégories).
def visualize_batches(batch_x, batch_y) :
fig = plt.figure(figsize=(15,15))
for i, (img, mask) in enumerate(zip(batch_img,batch_mask)):
ax = plt.subplot(BATCH_SIZE, 2, (i*2)+1)
print(img.shape)
plt.imshow((img*255).astype('uint8'))
ax.axis('off')
ax = plt.subplot(BATCH_SIZE, 2, (i*2)+2)
mmask = np.argmax(mask, axis=2)
plt.imshow(mmask)
ax.axis('off')
plt.tight_layout()
plt.show()
visualize_batches(batch_img, batch_mask)
(128, 256, 3) (128, 256, 3) (128, 256, 3) (128, 256, 3)
train_custom_augm = CustomDataGenerator(train_images_dir, train_masks_dir,
BATCH_SIZE, 128, 256, cats,
aug_blur=True, aug_zoom=True, aug_brightness=True, aug_flip=True)
len(train_custom_augm)
744
batch_img_aug, batch_mask_aug = train_custom_augm.__getitem__(100)
print(batch_img_aug.shape, batch_mask_aug.shape)
(20, 128, 256, 3) (20, 128, 256, 8)
visualize_batches(batch_img_aug, batch_mask_aug)
(128, 256, 3) (128, 256, 3) (128, 256, 3) (128, 256, 3)